import logging
import sqlite3
import threading
import time
import paho.mqtt.client as mqtt
import configparser
import re
from hawkeye import *
import os
import json
import base64
import signal
import queue

_mqtt_subscribe_topics = ["control/upload/#", "lily/+/msg"]
_mqtt_port = 1883
_mqtt_keep_alive = 600
_p_lily_msg = re.compile("lily/(\w+)/msg")
_p_lily_cmd = re.compile("lily/(\w+)/cmd/(\w+)")
_p_upload_control = re.compile("control/upload/(\w+)")
_mqtt_client_max_try_time = 5
_mqtt_last_retrieve_time = time.time()
_hawk_using_connection = None
_db = None
_log_file = None
_mqtt_server = "aahqtdc.iot.gz.baidubce.com"
_mqtt_username = None
_mqtt_password = None
_mqtt_client = None
_msg_queue = queue.Queue()


def create_db(fn):
    conn = sqlite3.connect(fn)
    cursor = conn.cursor()
    sql = """CREATE TABLE `hawkeye` (
        `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
        ,  `device` varchar(32) NOT NULL
        ,  `user` varchar(63) NOT NULL
        ,  `times` integer DEFAULT '0'
        ,  `condition` varchar(512) DEFAULT NULL
        ,  `action` varchar(512) DEFAULT NULL
        ,  `need_times` integer DEFAULT '1'
        ,  `trigger_time` timestamp NULL DEFAULT NULL
        ,  `min_period` integer DEFAULT '60'
        ,  `count` integer DEFAULT '0'
        ,  `ext` varchar(256) DEFAULT NULL
        ,  `type` varchar(16) DEFAULT 'off'
        ,  `error_count` integer DEFAULT '0'
        );"""
    cursor.execute(sql)
    conn.commit()
    cursor.close()
    conn.close()


def prepare():
    global _db, _log_file, _mqtt_username, _mqtt_password
    if not os.path.exists("hawk.ini"):
        logging.error("config file not found")
        exit(1)
    config = configparser.ConfigParser()
    config.read("hawk.ini")
    _db = config["database"]["db"]
    if not os.path.exists(_db):
        create_db(_db)
    _log_file = config["canary"]["log_file"]
    assert _log_file
    logging.basicConfig(
        level=logging.DEBUG,
        filename=_log_file,
        filemode="a",
        format="%(asctime)s %(filename)s/%(funcName)s/%(lineno)d [%(levelname)s]: %(message)s",
        datefmt="%Y/%m/%d %H:%M:%S",
    )
    _mqtt_username = config["canary"]["mqtt_username"]
    assert _mqtt_username
    _mqtt_subscribe_topics.append(f"lily/{_mqtt_username}/cmd/+")
    _mqtt_password = config["canary"]["mqtt_password"]
    assert _mqtt_password
    _mqtt_password = base64.b64decode(_mqtt_password).decode("utf-8")
    __wecom_user = config["mail"]["user"]
    __wecom_psw = config["mail"]["password"]
    assert __wecom_user and __wecom_psw
    __wecom_psw = base64.b64decode(__wecom_psw).decode("utf-8")
    setup_mail(__wecom_user, __wecom_psw)


def on_connect(client, userdata, flags, rc, properties):
    global _mqtt_client_max_try_time, _mqtt_last_retrieve_time
    if rc == 4:
        logging.error("username or password error")
        _mqtt_client.disconnect()
        quit()
        return
    if rc:
        _mqtt_client_max_try_time -= 1
        if _mqtt_client_max_try_time <= 0:
            logging.error("max try connect times exceed, connect failed")
            quit()
            return
    else:
        if time.time() - _mqtt_last_retrieve_time > 3600:
            _mqtt_client_max_try_time = 5


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    global _mqtt_client_max_try_time
    _mqtt_client_max_try_time -= 1
    if _mqtt_client_max_try_time <= 0:
        logging.error("max try connect times exceed, connect failed")
        quit()
        return


def publish_msg(msg, topic=None):
    assert _mqtt_client
    assert isinstance(msg, str) or isinstance(msg, bytes)
    if not topic:
        topic = f"lily/{_mqtt_username}/msg"
    else:
        assert isinstance(topic, str)
    _mqtt_client.publish(topic, msg)


def cmd_select(args):
    db_cursor = get_cursor()
    sql = "SELECT * FROM hawkeye"
    if "device" in args or "user" in args:
        sql += f" WHERE"
        lk = None
        for k in ["device", "user"]:
            if k not in args:
                continue
            if lk:
                sql += " AND"
            sql += f" {k}='{args[k]}'"
            lk = k
    db_cursor.execute(sql)
    res = db_cursor.fetchall()
    commit_cursor(db_cursor)
    msg = json.dumps(res)
    publish_msg(msg)


def _str_to_hex(s):
    return "CAST(X'%s' AS TEXT)" % s.encode("utf-8").hex()


def _filter_args(args):
    allowed_keys = [
        "device",
        "user",
        "condition",
        "action",
        "need_times",
        "type",
        "min_period",
    ]
    keys = {k: args[k] for k in allowed_keys if k in args}
    for k in ["condition", "action", "device", "user", "type"]:
        if k not in keys:
            continue
        assert isinstance(keys[k], str)
        keys[k] = _str_to_hex(keys[k])

    for k in ["need_times", "min_period"]:
        if k not in keys:
            continue
        assert isinstance(keys[k], int)
    return keys


def cmd_insert(args):
    if "device" not in args or "user" not in args:
        logging.error(f"device or user not found in args for {args}")
        return
    keys = _filter_args(args)
    sql = "INSERT INTO hawkeye (%s) VALUES (%s);" % (
        ",".join(keys.keys()),
        ",".join([f"{v}" for v in keys.values()]),
    )
    db_cursor = get_cursor()
    db_cursor.execute(sql)
    commit_cursor(db_cursor)


def cmd_update(args):
    if "id" not in args:
        logging.error(f"id not found in args for '{args}'")
        return
    keys = _filter_args(args)
    sql = "UPDATE hawkeye SET %s WHERE id=%d;" % (
        ",".join([f"{k}={v}" for k, v in keys.items()]),
        args["id"],
    )
    db_cursor = get_cursor()
    db_cursor.execute(sql)
    commit_cursor(db_cursor)


def cmd_delete(args):
    assert "id" in args
    col_id = args.get("id")
    db_cursor = get_cursor()
    db_cursor.execute(f"DELETE FROM hawkeye WHERE id={col_id}")
    commit_cursor(db_cursor)


def on_message(client_name, userdata, msg):
    topic = msg.topic
    _msg_queue.put_nowait((topic, msg.payload))


def get_cursor():
    global _hawk_using_connection
    if not _hawk_using_connection:
        _hawk_using_connection = sqlite3.connect(_db)
    return _hawk_using_connection.cursor()


def commit_cursor(cursor):
    cursor.close()
    if _hawk_using_connection:
        _hawk_using_connection.commit()


def _msg_loop(topic, payload):
    res = re.match(_p_lily_msg, topic)
    running_context = {}
    if not res:
        res = re.match(_p_upload_control, topic)
        running_context = "upload"
    if not res:
        res = re.match(_p_lily_cmd, topic)
        running_context = "cmd"
    if not res:
        return
    device = res.group(1)
    message = payload.decode("utf-8", "ignore")
    if running_context == "upload":
        try:
            running_context = json.loads(message)
        except Exception as e:
            running_context = {}
    elif running_context == "cmd":
        logging.info(f"cmd message: {topic}: {message}")
        try:
            args = json.loads(message)
        except Exception as e:
            logging.error(f"json loads failed: {e}")
            args = {}
        cmd = res.group(2)
        if cmd == "select":
            cmd_select(args)
        elif cmd == "insert":
            cmd_insert(args)
        elif cmd == "update":
            cmd_update(args)
        elif cmd == "delete":
            cmd_delete(args)
        else:
            logging.error(f"unknown cmd '{cmd}'")
        return
    running_context["msg"] = message
    running_context["topic"] = topic
    running_context["device"] = device

    db_cursor = get_cursor()
    hawkeye_sweep_device(db_cursor, device, running_context)
    commit_cursor(db_cursor)


def main():
    global _mqtt_client
    _mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    _mqtt_client.on_connect = on_connect
    _mqtt_client.on_disconnect = on_disconnect
    _mqtt_client.on_message = on_message
    _mqtt_client.on_connect_fail = lambda a, b, c: print("connect failed")
    _mqtt_client.username_pw_set(
        f"thingidp@aahqtdc|{_mqtt_username}|0|MD5", _mqtt_password
    )
    _mqtt_client.connect(_mqtt_server, _mqtt_port, _mqtt_keep_alive)
    for topic in _mqtt_subscribe_topics:
        _mqtt_client.subscribe(topic)
    th_mail_process = threading.Thread(target=delivery_mail_thread)
    setups(False, _mqtt_client, logging)
    th_mail_process.start()
    get_cursor().close()
    _mqtt_client.loop_start()
    while "you are good":
        got = _msg_queue.get()
        if not got:
            break
        topic, payload = got
        if not topic:
            continue
        _msg_loop(topic, payload)


def quit():
    _msg_queue.put(None)


if __name__ == "__main__":
    prepare()
    signal.signal(signal.SIGINT, lambda a, b: quit())
    try:
        main()
    except Exception as e:
        logging.error(f"exception: {e}")
    except KeyboardInterrupt:
        pass
    finally:
        if _hawk_using_connection:
            _hawk_using_connection.close()
        if _mqtt_client:
            _mqtt_client.disconnect()
        logging.info("client stopped")
        turn_off_process(True)
        _msg_queue.put(None)
